//	CP2013 Traffic Simulator Assignment
//      Traffic Model Controller
//	Java version
//      Coded by Matthew Johnson


package traffic;

import java.util.Random;
import java.util.Vector;

//Controller needs to be a Thread so that the visualisation will update properly
public class Controller extends Thread
{
    //The model of the intersection
    private Model model;
    //The random number generator
    private Random rNumGen;
    //A reference to the graphical display panel for refreshing
    private GraphicPanel graphicPanel;
    //Stores the number of cycles to run
    private int vehThroughInter;
    //Counts the number of times a lane fills up
    private int trafficBackedUp;

    //Constructor: takes a model and the grapical display panel
    Controller(Model m, GraphicPanel g)
    {

        model = m;

        graphicPanel = g;

        rNumGen = new Random();
    }



    //Runs the set number of cycles
    public void run()
    {

        //Initialise database statistic tracking
        vehThroughInter = 0;
        trafficBackedUp = 0;

        //Run the specified number of cycles
        for (int i = 1; i <= model.settings.numCycles; ++i)
        {

            //Run a cycle
            runCycle(i, model.settings.numCycles);

            //Redraw the graphical display
            graphicPanel.repaint();

            //Pause for one fifth of a second
            try
            {
                Thread.currentThread().sleep(200);
            } 
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
        //Write to database
        try
        {
            DatabaseAccess db = new DatabaseAccess(DatabaseAccess.DEFAULT_DATABASE);
            db.insertResults(model.settings, vehThroughInter);
            db.close();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }

    }

    //Runs a single cycle
    private void runCycle(int i, int t)
    {
        //Move the existing Vehicles
        moveVehicles();
        //Spawn new Vehicles
        spawnVehicles();
        //Change the Lights
        cycleLights();
    }

    //Removes vehicles that have reached the edge of the map and moves the
    //existing vehicles
    private void moveVehicles()
    {
        //A copy of the vehicle Vector is used to prevent a
        //concurrent modification exception upon deleting vehicles
        Vector<Vehicle> temp = new Vector<Vehicle>(model.vehicles);

        //Loop through each of the vehicles and move them forward if possible
        for (Vehicle currVehicle : temp)
        {
            //This will hold the target tile's coordinates
            Coordinate target = new Coordinate(currVehicle.getPosition());

            //Tracks if the vehicle has moved.
            boolean hasMoved = false;

            //Vehicles with speed greater than one, will advance multiple tiles.
            for (int spaceN = currVehicle.getSpeed(); spaceN > 0; --spaceN)
            {
                //If the vehicle is driving off of the map
                if (leavingWorld(currVehicle))
                {
                    //Delete it from the Vector of Vehicles
                    model.vehicles.remove(currVehicle);
                    hasMoved = true;
                }
                else
                {
                    //Set the target tile based on the Vehicle's direction
                    switch (currVehicle.getDirection())
                    {
                        case NORTH:
                            target.y -= 1;
                            break;

                        case SOUTH:
                            target.y += 1;
                            break;

                        case WEST:
                            target.x -= 1;
                            break;

                        case EAST:
                            target.x += 1;
                            break;
                    }

                    //If the target tile is free and flows in the same direction
                    if (!isObstructed(target, currVehicle.getDirection()))
                    {
                        //If we are entering the intersection, increment the counter
                        if (isEnteringIntersection(currVehicle))
                        {
                            vehThroughInter++;
                        }

                        //Move the Vehicle
                        currVehicle.setPosition(target);
                        hasMoved = true;
                    }
                }
            }

            //If the vehicle hasn't moved, try overtaking
            if (!hasMoved)
            {

                //Determine the diagonally forward and sideways tiles
                Coordinate diagLeft, diagRight, sideLeft, sideRight;
                diagLeft = new Coordinate(currVehicle.getPosition());
                diagRight = new Coordinate(currVehicle.getPosition());
                sideLeft = new Coordinate(currVehicle.getPosition());
                sideRight = new Coordinate(currVehicle.getPosition());
                int w = 1, h = 1;

                //Determine the target tiles based on the vehicle's direction
                switch (currVehicle.getDirection())
                {
                    case NORTH:
                        //Vehicle's Left is West
                        diagLeft.x -= 1;
                        sideLeft.x -= 1;

                        //Vehicle's Right is East
                        diagRight.x += 1;
                        sideRight.x += 1;

                        //Ahead is North
                        diagLeft.y -= 1;
                        diagRight.y -= 1;

                        //Behind is South
                        h = currVehicle.getLength();
                        break;

                    case SOUTH:
                        //Vehicle's Left is East
                        diagLeft.x += 1;
                        sideLeft.x += 1;

                        //Vehicle's Right is West
                        diagRight.x -= 1;
                        sideRight.x -= 1;

                        //Ahead is South
                        diagLeft.y += 1;
                        diagRight.y += 1;

                        //Behind is North
                        if (currVehicle.getLength() > 1)
                        {
                            h = 0 - (currVehicle.getLength() - 1);
                        } else
                        {
                            h = currVehicle.getLength();
                        }
                        break;

                    case WEST:
                        //Vehicle's Left is South
                        diagLeft.y += 1;
                        sideLeft.y += 1;

                        //Vehicle's Right is North
                        diagRight.y -= 1;
                        sideRight.y -= 1;

                        //Ahead is West
                        diagLeft.x -= 1;
                        diagRight.x -= 1;

                        //Behind is East
                        w = currVehicle.getLength();
                        break;

                    case EAST:
                        //Vehicle's Left is North
                        diagLeft.y -= 1;
                        sideLeft.y -= 1;

                        //Vehicle's Right is South
                        diagRight.y += 1;
                        sideRight.y += 1;

                        //Ahead is East
                        diagLeft.x += 1;
                        diagRight.x += 1;

                        //Behind is West
                        if (currVehicle.getLength() > 1)
                        {
                            w = 0 - (currVehicle.getLength() - 1);
                        } else {
                            w = currVehicle.getLength();
                        }
                        break;
                }

                //Stores whether a valid overtake exists
                boolean doOvertake = false;

                //If the left diagonal and sideways tiles are free, move left
                if (!isObstructed(diagLeft, currVehicle.getDirection()) && !isObstructed(sideLeft, w, h, currVehicle.getDirection()))
                {
                    //Overtake left
                    target = sideLeft;
                    doOvertake = true;
                }
                //If left is invalid, try right
                else if (!isObstructed(diagRight, currVehicle.getDirection()) && !isObstructed(sideRight, w, h, currVehicle.getDirection()))
                {
                    //Overtake right
                    target = sideRight;
                    doOvertake = true;
                }

                //If we can overtake
                if (doOvertake)
                {
                    //Move to the target tile
                    currVehicle.setPosition(target);
                }
            }
        }
    }

    private void spawnVehicles()
    {

        //Loop through each of the lanes and spawn cars based on probability
        for (Road r : model.roads)
        {
            for (Lane l : r.lanes)
            {

                //Generate a random number from one to one hundred (inclusive)
                int randNum = rNumGen.nextInt(100) + 1;

                //If the random number is lower or equal to the probability, spawn a car
                if (randNum <= l.probability)
                {
                    //Check that the spawn point is not obstructed
                    if (isObstructed(l.spawnPoint, Direction.UNDEFINED))
                    {
                        //If it is obstructed then record it
                        trafficBackedUp++;
                    } 
                    else
                    {
                        //Create a new car at the spawn point
                        Vehicle newCar = new Vehicle(l.spawnPoint, l.direction, 1);

                        //Add the new car
                        model.vehicles.add(newCar);
                    }
                }
            }
        }
    }

    //Cycles the intersection to the next state
    private void cycleLights()
    {
        //If there is a yellow light present, we need to check that the intersection is clear
        if (model.intSct.yellowOn())
        {
            if (!isObstructed(new Coordinate(model.intSct.getAnchor()), model.intSct.getWidth(), model.intSct.getHeight(), Direction.UNDEFINED))
            {
                //The intersection is clear, move to the next state in the cycle
                model.intSct.cycleState();
                //Update the map
                updateIntersectionTiles();
            }
        } 
        else
        {
            //No yellow present, move to the next state in the cycle
            model.intSct.cycleState();
            //Update the map
            updateIntersectionTiles();
        }
    }

    //Test if a Vehicle is occupying a square and if the direction is the same
    private boolean isObstructed(Coordinate t, Direction dir)
    {
        //First, if a direction has been supplied, the target tile must support it
        if (dir != Direction.UNDEFINED && dir != model.map.directionAt(t))
        {
            return true;
        }

        //Loop through each of the vehicles and check their size and position
        for (Vehicle currVehicle : model.vehicles)
        {
            //If the vehicle is only one tile long, simply check if its position matches
            if (currVehicle.getLength() == 1)
            {
                if (currVehicle.getPosition().equals(t))
                {
                    return true;
                }
            } 
            else
            {
                //Determine which tiles the vehicle obstructs
                for (int j = 0; j < currVehicle.getLength(); ++j)
                {
                    //Start with the vehicle location
                    Coordinate obsTile = new Coordinate(currVehicle.getPosition());

                    //Based on the direction of the vehicle, proceed outward
                    switch (currVehicle.getDirection())
                    {
                        case NORTH:
                            obsTile.y += j;
                            break;

                        case SOUTH:
                            obsTile.y -= j;
                            break;

                        case WEST:
                            obsTile.x += j;
                            break;

                        case EAST:
                            obsTile.x -= j;
                            break;
                    }

                    //If the obstructed tile is the target, return true
                    if (obsTile == t)
                    {
                        return true;
                    }
                }
            }
        }

        //If we get to this line, the tile is unobstructed
        return false;
    }

    //Tests if a range of tiles is being obstucted and have the same direction
    private boolean isObstructed(Coordinate base, int w, int h, Direction dir) 
    {
        //For each tile in the range
        for (int y = 0; y < h; ++y)
        {
            for (int x = 0; x < w; ++x) 
            {
                //Get the coordinate
                Coordinate c = new Coordinate(base);
                c.x += x;
                c.y += y;
                //and test it
                if (isObstructed(c, dir))
                {
                    return true;
                }
            }
        }
        return false;
    }

    //Test if a vehicle has reached the edge of the map
    private boolean leavingWorld(Vehicle v) 
    {
        //The map edge to test depends on the Vehicle's direction
        switch (v.getDirection())
        {
            case NORTH:
                if (v.getPosition().y == 0)
                {
                    return true;
                }
                break;

            case WEST:
                if (v.getPosition().x == 0)
                {
                    return true;
                }
                break;

            case EAST:
                if (v.getPosition().x == model.map.getWidth() - 1)
                {
                    return true;
                }
                break;

            case SOUTH:
                if (v.getPosition().y == model.map.getHeight() - 1)
                {
                    return true;
                }
                break;
        }
        return false;
    }

    private boolean isEnteringIntersection(Vehicle v)
    {
        //Loop through the lanes to determine if the vehicle is on the intersection entry point
        for (Road r : model.roads)
        {
            for (Lane l : r.lanes)
            {
                //Grab a reference to the current lane's intersection entry point
                Coordinate entryPoint = l.intersectionEntryPoint;

                //Only bother checking anything if the lane actually has an intersection entry point
                if (entryPoint != null)
                {
                    //To be entering the intersection, a vehicle must be on the entry point facing the same way as the lane
                    if (v.getPosition().equals(entryPoint) && (l.direction == v.getDirection()))
                    {
                        return true;
                    }
                }
            }
        }

        //The vehicle is not entering the intersection
        return false;
    }

    //Refreshes the Intersection's current direction in the TileMap
    private void updateIntersectionTiles()
    {
        //If no yellow is present, a green must be present, so we can fill all of the tiles
        if (!model.intSct.yellowOn())
        {
            //Create a temporary variable to hold the new tile direction
            Direction dir;

            //Determine which direction the green is for
            if (model.intSct.getVLight().state == LightState.GREEN)
            {
                dir = Direction.NORTH;
            } 
            else
            {
                dir = Direction.EAST;
            }

            //Loop through all of the tiles and update their directions
            model.map.setTileRange(new Coordinate(model.intSct.getAnchor()), model.intSct.getWidth(),
                    model.intSct.getHeight(), dir);

        }
        else
        {
            //A yellow is present, so we need to set two particular edges of the intersection to be block incoming traffic
            Coordinate base = new Coordinate(model.intSct.getAnchor());

            //Determine which direction the yellow is for
            if (model.intSct.getVLight().state == LightState.YELLOW)
            {
                //The yellow is for North/South, so we set the top and bottom edges to block
                model.map.setTileRange(base, model.intSct.getWidth(), 1, Direction.NORTH);
                base.y += (model.intSct.getHeight() - 1);
                model.map.setTileRange(base, model.intSct.getWidth(), 1, Direction.SOUTH);
            } 
            else
            {
                //The yellow is for West/East, so we set the left and right edges to block
                model.map.setTileRange(base, 1, model.intSct.getHeight(), Direction.WEST);
                base.x += (model.intSct.getWidth() - 1);
                model.map.setTileRange(base, 1, model.intSct.getHeight(), Direction.EAST);
            }
        }
    }
}
